跳到主要内容

OpenGL 贴图绘制

纹理的基本概念

纹理坐标概念

为了能够把纹理映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样

我们为三角形指定了 3 个纹理坐标点,纹理坐标看起来就像这样:

var (
texCoords = []float32{
0.0, 0.0, // 左下角
1.0, 0.0, // 右下角
0.5, 1.0, // 上中
}
)

纹理环绕方式

纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL 默认的行为是重复这个纹理图像

也可以设置如下这些效果:

环绕方式描述
REPEAT对纹理的默认行为。重复纹理图像。
MIRRORED_REPEAT和 REPEAT 一样,但每次重复图片是镜像放置的。
CLAMP_TO_EDGE纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘。
CLAMP_TO_BORDER超出的坐标为用户指定的边缘颜色。

前面提到的每个选项都可以使用 gl.TexParameteri 函数对单独的一个坐标轴设置

// 对应 X
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT)
// 对应 Y
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT)

如果是 3D 纹理还有一个 R 轴

纹理过滤

纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以 OpenGL 需要知道怎样将纹理像素映射到纹理坐标。

OpenGL 也有对于纹理过滤(Texture Filtering)的选项。纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST 和 GL_LINEAR。

GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是 OpenGL 默认的纹理过滤方式。当设置为 GL_NEAREST 的时候, OpenGL 会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:

那么这两种纹理过滤方式有怎样的视觉效果呢?

当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。

// 放大
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// 缩小
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)

多级渐远纹理

假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL 从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

OpenGL 使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一

在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用 NEAREST 和 LINEAR 过滤。

可以使用下面四个选项中的一个代替原有的过滤方式:

过滤方式描述
NEAREST_MIPMAP_NEAREST使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
LINEAR_MIPMAP_NEAREST使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
NEAREST_MIPMAP_LINEAR在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
LINEAR_MIPMAP_LINEAR在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
// 放大
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
// 缩小
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST)

加载与创建纹理

生成纹理

和之前生成的 OpenGL 对象一样,纹理也是使用ID引用的

var texture uint32
gl.GenTextures(1, &texture)

GenTextures 函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的 uint32 数组中(这里只有一张纹理,所以传入一个)

然后绑定纹理

gl.BindTexture(gl.TEXTURE_2D, texture)

现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过 gl.TexImage2D 来生成:

gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, uint32(gl.UNSIGNED_BYTE), gl.Ptr(rgba.Pix))
  • 第一个参数指定了纹理目标(Target)。设置为 TEXTURE_2D 意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到 TEXTURE_1D 和 TEXTURE_3D 的纹理不会受到影响)。

  • 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话,这里我们填 0,也就是基本级别。

  • 第三个参数告诉 OpenGL 我们希望把纹理储存为何种格式,我们的图像只有 RGB 值,因此我们也把纹理储存为 RGB 值。

  • 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。

  • 下个参数应该总是被设为 0(历史遗留的问题)。

  • 第七第八个参数定义了源图的格式和数据类型,我们使用 RGB 值加载这个图像,并把它们储存为 byte 数组,我们将会传入对应值。

  • 最后一个参数是真正的图像数据。

生成一个纹理的过程应该看起来像这样:

var handle uint32
gl.GenTextures(1, &handle)

target := uint32(gl.TEXTURE_2D)
internalFmt := int32(gl.SRGB_ALPHA)
format := uint32(gl.RGBA)
width := int32(rgba.Rect.Size().X)
height := int32(rgba.Rect.Size().Y)
pixType := uint32(gl.UNSIGNED_BYTE)
dataPtr := gl.Ptr(rgba.Pix)

texture := Texture{
handle:handle,
target:target,
}

texture.Bind(gl.TEXTURE0)
defer texture.UnBind()

// 为当前绑定的纹理对象设置环绕、过滤方式
gl.TexParameteri(texture.target, gl.TEXTURE_WRAP_R, wrapR)
gl.TexParameteri(texture.target, gl.TEXTURE_WRAP_S, wrapS)
gl.TexParameteri(texture.target, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // minification filter
gl.TexParameteri(texture.target, gl.TEXTURE_MAG_FILTER, gl.LINEAR) // magnification filter


gl.TexImage2D(target, 0, internalFmt, width, height, 0, format, pixType, dataPtr)
gl.GenerateMipmap(texture.handle)